﻿#region Usings
using System;
using System.Linq;
using CID.Domain.Audit;
using CID.Domain.Development;
using CID.Domain.Operations;
using CID.Domain.Users;
using CID.Execution.Support.Contracts;
using CID.Execution.Support.Exceptions;
using NailsFramework.IoC;
using NailsFramework.Persistence;
using CID.Execution.Controller.Support;
using NailsFramework.Aspects;
using NailsFramework.UnitOfWork;
using System.Reflection;
using System.Collections.Generic;
using CID.Execution.Support.DeploymentInitiation;
using Environment = CID.Domain.Operations.Environment;
using CID.Execution.Controller.Exceptions;
#endregion

namespace CID.Execution.Controller
{
    [Lemming]
    public class Controller : IController
    {
        private const string GerenalDeactivationErrorMessage = "The Deactivation failed due to a previous error";
        private const string GeneralDeploymentErrorMessage = "The Deployment failed due to a previous error";
        private const string GeneralActivationErrorMessage = "The Activation failed due to a previous error";

        [Inject]
        public IBag<Release> Releases { private get; set; }
        [Inject]
        public IBag<Environment> Environments { private get; set; }
        [Inject]
        public IBag<User> Users { private get; set; }
        [Inject]
        public IBag<Location> Locations { private get; set; }
        [Inject]
        public IBag<ReleaseDeploymentLog> DeploymentLogs { private get; set; }

        public virtual Guid InitiateDeployment(string releaseName, string targetEnvironmentName, string responsibleUserName)
        {
                var release = Releases.SingleOrDefault(x => x.Name == releaseName);
                var targetEnvironment = Environments.SingleOrDefault(x => x.Name == targetEnvironmentName);
                var responsible = Users.SingleOrDefault(x => x.UserName == responsibleUserName);
                return Deploy(release, targetEnvironment, responsible);
        }

        public virtual Guid InitiateDeployment(int releaseId, int targetEnvironmentId, int responsibleId)
        {
            return Deploy(Releases.GetById(releaseId), Environments.GetById(targetEnvironmentId), Users.GetById(responsibleId));
        }

        private Guid Deploy(IRelease release, IEnvironment targetEnvironment, IUser responsible)
        {
            if (release == null) throw new InvalidDeploymentInitiationException("Release");
            if (targetEnvironment == null) throw new InvalidDeploymentInitiationException("Target Environment");
            if (responsible == null) throw new InvalidDeploymentInitiationException("Responsible User");
            if (!(responsible.Is(UserRole.Manager) || responsible.Is(UserRole.Operator)))
                throw new UserActionUnauthorizedException("deployment initiation", UserRole.Manager, UserRole.Operator);

            var versionsMissingDeploymentPackage = release.Versions.Where(x => x.ActivePackage == null);
            if (versionsMissingDeploymentPackage.Count() > 0) 
                throw new MissingVersionDeploymentPackageException(versionsMissingDeploymentPackage);

            var deploymentLog = new ReleaseDeploymentLog(release, targetEnvironment, responsible);
            DeploymentLogs.Put(deploymentLog);
            var deploymentState = new DeploymentState(deploymentLog.DeploymentUniqueIdentifier, release, targetEnvironment);
            DeploymentStateRepository.Instance.Add(deploymentState);

            deploymentLog.Start();
            StartDeactivation(deploymentState);

            return deploymentLog.DeploymentUniqueIdentifier;
        }

        private void StartDeactivation(IDeploymentState deploymentState)
        {
            foreach (var deactivationScript in deploymentState.DeactivationScripts)
            {
                try
                {
                    var location = Locations.GetById(deactivationScript.LocationId);
                    var agentService = AgentServiceProxyFactory.Instance.Create(location.DeploymentAgent);
                    agentService.StartDeactivation(deactivationScript);
                    AgentServiceProxyFactory.Instance.Dispose(agentService);
                }
                catch (Exception ex)
                {
                    LogStageStartError(deactivationScript.DeploymentUniqueIdentifier, "Deactivation", deactivationScript.LocationId, ex);
                    NotifyDeactivationCompleted(deactivationScript.DeploymentUniqueIdentifier, deactivationScript.LocationId, GerenalDeactivationErrorMessage, true);
                    //NotifyDeploymentCompleted(deactivationScript.DeploymentUniqueIdentifier, deactivationScript.LocationId, GeneralDeploymentErrorMessage, true);
                    //NotifyActivationCompleted(deactivationScript.DeploymentUniqueIdentifier, deactivationScript.LocationId, GeneralActivationErrorMessage, true);
                }
            }
        }

        public virtual void NotifyDeactivationCompleted(Guid deploymentUniqueIdentifier, int locationId, string details, bool error)
        {
            var deploymentState = DeploymentStateRepository.Instance.Get(deploymentUniqueIdentifier);
            var pendingDeactivations = deploymentState.NotifyDeactivationCompleted(locationId, error);
            LogStageCompleted(deploymentUniqueIdentifier, "Deactivation", locationId, details, error);
            if (pendingDeactivations == 0)
                StartDeployment(deploymentState);
        }

        private void StartDeployment(IDeploymentState deploymentState)
        {
            foreach (var deploymentScript in deploymentState.DeploymentScripts)
            {
                try
                {
                    if (!deploymentState.LocationHasErrors(deploymentScript.LocationId))
                    {
                        var location = Locations.GetById(deploymentScript.LocationId);
                        var agentService = AgentServiceProxyFactory.Instance.Create(location.DeploymentAgent);
                        agentService.StartDeployment(deploymentScript);
                        AgentServiceProxyFactory.Instance.Dispose(agentService);
                    }
                    else
                        SkipDeployment(deploymentState, deploymentScript.LocationId);
                }
                catch (Exception ex)
                {
                    LogStageStartError(deploymentScript.DeploymentUniqueIdentifier, "Deployment", deploymentScript.LocationId, ex);
                    NotifyDeploymentCompleted(deploymentScript.DeploymentUniqueIdentifier, deploymentScript.LocationId, GeneralDeploymentErrorMessage, true);
                    //NotifyActivationCompleted(deploymentScript.DeploymentUniqueIdentifier, deploymentScript.LocationId, GeneralActivationErrorMessage, true);
                }
            }
        }

        private void SkipDeployment(IDeploymentState deploymentState, int locationId)
        {
            LogStageSkipped(deploymentState.DeploymentUniqueIdentifier, "Deployment", locationId);
            OnDeploymentDone(deploymentState, locationId);
        }

        public virtual void NotifyDeploymentCompleted(Guid deploymentUniqueIdentifier, int locationId, string details, bool error)
        {
            var deploymentState = DeploymentStateRepository.Instance.Get(deploymentUniqueIdentifier);
            LogStageCompleted(deploymentUniqueIdentifier, "Deployment", locationId, details, error);
            OnDeploymentDone(deploymentState, locationId, error);
        }

        private void OnDeploymentDone(IDeploymentState deploymentState, int locationId, bool error = false)
        {
            var pendingDeployments = deploymentState.NotifyDeploymentCompleted(locationId, error);
            if (pendingDeployments == 0)
                StartActivation(deploymentState);
        }

        private void StartActivation(IDeploymentState deploymentState)
        {
            foreach (var activationScript in deploymentState.ActivationScripts)
            {
                try
                {
                    if (!deploymentState.LocationHasErrors(activationScript.LocationId))
                    {
                        var location = Locations.GetById(activationScript.LocationId);
                        var agentService = AgentServiceProxyFactory.Instance.Create(location.DeploymentAgent);
                        agentService.StartActivation(activationScript);
                        AgentServiceProxyFactory.Instance.Dispose(agentService);
                    }
                    else
                        SkipActivation(deploymentState, activationScript.LocationId);
                }
                catch (Exception ex)
                {
                    LogStageStartError(activationScript.DeploymentUniqueIdentifier, "Activation", activationScript.LocationId, ex);
                    NotifyActivationCompleted(activationScript.DeploymentUniqueIdentifier, activationScript.LocationId, GeneralActivationErrorMessage, true);
                }
            }
        }
        
        private void SkipActivation(IDeploymentState deploymentState, int locationId)
        {
            LogStageSkipped(deploymentState.DeploymentUniqueIdentifier, "Activation", locationId);
            OnActivationDone(deploymentState, locationId);
        }

        public virtual void NotifyActivationCompleted(Guid deploymentUniqueIdentifier, int locationId, string details, bool error)
        {
            var deploymentState = DeploymentStateRepository.Instance.Get(deploymentUniqueIdentifier);
            LogStageCompleted(deploymentUniqueIdentifier, "Activation", locationId, details, error);
            OnActivationDone(deploymentState, locationId, error);
        }

        private void OnActivationDone(IDeploymentState deploymentState, int locationId, bool error = false)
        {
            var pendingActivations = deploymentState.NotifyActivationCompleted(locationId, error);
            if (pendingActivations == 0)
                EndDeployment(deploymentState);
        }

        private void EndDeployment(IDeploymentState deploymentState)
        {
            var deploymentLog = DeploymentLogs.SingleOrDefault(x => x.DeploymentUniqueIdentifier == deploymentState.DeploymentUniqueIdentifier);
            deploymentLog.End(deploymentState.Successful);
            
            if(deploymentState.Successful) {
                deploymentLog.Environment.RegisterReleaseDeployment(deploymentLog.Release);
                if (deploymentLog.Environment.IsProduction)
                    deploymentLog.Release.SetReleased();
            }

            DeploymentStateRepository.Instance.Remove(deploymentState);
        }

        private void LogStageCompleted(Guid deploymentUniqueIdentifier, string stage, int locationId, string details, bool error)
        {
            var deploymentLog = DeploymentLogs.SingleOrDefault(x => x.DeploymentUniqueIdentifier == deploymentUniqueIdentifier);
            var location = Locations.GetById(locationId);
            string headerMessage = error ?
                String.Format("The {0} stage ended with errors in location {1}", stage, location.Name) :
                String.Format("The {0} stage successfully ended in location {1}", stage, location.Name);

            deploymentLog.AppendDetails(headerMessage);
            deploymentLog.AppendDetails(details);
        }

        private void LogStageStartError(Guid deploymentUniqueIdentifier, string stage, int locationId, Exception ex)
        {
            var deploymentLog = DeploymentLogs.SingleOrDefault(x => x.DeploymentUniqueIdentifier == deploymentUniqueIdentifier);
            var location = Locations.GetById(locationId);
            string headerMessage = String.Format("The {0} stage failed to start in location {1}", stage, location.Name);

            deploymentLog.AppendDetails(headerMessage);
            deploymentLog.AppendDetails(ex.ToString());
        }

        private void LogStageSkipped(Guid deploymentUniqueIdentifier, string stage, int locationId)
        {
            var deploymentLog = DeploymentLogs.SingleOrDefault(x => x.DeploymentUniqueIdentifier == deploymentUniqueIdentifier);
            var location = Locations.GetById(locationId);
            string message = String.Format("The {0} stage was skipped in location {1} due to previous errors", stage, location.Name);

            deploymentLog.AppendDetails(message);
        }
    }
}
